Инвесторы из фонда «Shut Up and Take My Money» решили попробовать себя в новой области и открыть заведение общественного питания в Москве. Заказчики ещё не знают, что это будет за место: кафе, ресторан, пиццерия, паб или бар, — и какими будут расположение, меню и цены.
Цель:
Исследовать рынок Москвы, найти интересные особенности и презентовать полученные результаты, которые в будущем помогут в выборе подходящего инвесторам места.
Задачи:
Описание данных:
Доступен датасет с заведениями общественного питания Москвы, составленный на основе данных сервисов Яндекс Карты и Яндекс Бизнес на лето 2022 года.
Файл moscow_places.csv
:
name
— название заведения;
address
— адрес заведения;
category
— категория заведения, например «кафе», «пиццерия» или «кофейня»;
hours
— информация о днях и часах работы;
lat
— широта географической точки, в которой находится заведение;
lng
— долгота географической точки, в которой находится заведение;
rating
— рейтинг заведения по оценкам пользователей в Яндекс Картах (высшая оценка — 5.0);
price
— категория цен в заведении, например «средние», «ниже среднего», «выше среднего» и так далее;
avg_bill
— строка, которая хранит среднюю стоимость заказа в виде диапазона, например:
middle_avg_bill
— число с оценкой среднего чека, которое указано только для значений из столбца avg_bill
, начинающихся с подстроки «Средний счёт»:
middle_coffee_cup
— число с оценкой одной чашки капучино, которое указано только для значений из столбца avg_bill
, начинающихся с подстроки «Цена одной чашки капучино»:
chain
— число, выраженное 0 или 1, которое показывает, является ли заведение сетевым (для маленьких сетей могут встречаться ошибки);
district
— административный район, в котором находится заведение, например Центральный административный округ;
seats
— количество посадочных мест.
import re
import json
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import plotly.express as px
import warnings; warnings.filterwarnings(action='ignore')
from sklearn.impute import KNNImputer
from plotly import graph_objects as go
from folium import Map, Choropleth
from folium import Map, Marker # импортируем карту и маркер
from folium.plugins import MarkerCluster # импортируем кластер
from folium import Map, Choropleth # импортируем карту и хороплет
# снимаем ограничение на количество столбцов
pd.set_option('display.max_columns', None)
# снимаем ограничение на ширину столбцов
pd.set_option('display.max_colwidth', None)
# игнорируем предупреждения
pd.set_option('chained_assignment', None)
# выставляем ограничение на показ знаков после запятой
pd.options.display.float_format = '{:,.2f}'.format
# устанавливаем стиль графиков
sns.set_style('whitegrid')
sns.set(rc={"figure.dpi":200, 'savefig.dpi':300})
sns.set_context('notebook')
sns.set_style("ticks")
try:
data = pd.read_csv('/datasets/moscow_places.csv')
except:
data = pd.read_csv('moscow_places.csv')
display(data.head(), data.sample(5), data.tail())
name | category | address | district | hours | lat | lng | rating | price | avg_bill | middle_avg_bill | middle_coffee_cup | chain | seats | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | WoWфли | кафе | Москва, улица Дыбенко, 7/1 | Северный административный округ | ежедневно, 10:00–22:00 | 55.88 | 37.48 | 5.00 | NaN | NaN | NaN | NaN | 0 | NaN |
1 | Четыре комнаты | ресторан | Москва, улица Дыбенко, 36, корп. 1 | Северный административный округ | ежедневно, 10:00–22:00 | 55.88 | 37.48 | 4.50 | выше среднего | Средний счёт:1500–1600 ₽ | 1,550.00 | NaN | 0 | 4.00 |
2 | Хазри | кафе | Москва, Клязьминская улица, 15 | Северный административный округ | пн-чт 11:00–02:00; пт,сб 11:00–05:00; вс 11:00–02:00 | 55.89 | 37.53 | 4.60 | средние | Средний счёт:от 1000 ₽ | 1,000.00 | NaN | 0 | 45.00 |
3 | Dormouse Coffee Shop | кофейня | Москва, улица Маршала Федоренко, 12 | Северный административный округ | ежедневно, 09:00–22:00 | 55.88 | 37.49 | 5.00 | NaN | Цена чашки капучино:155–185 ₽ | NaN | 170.00 | 0 | NaN |
4 | Иль Марко | пиццерия | Москва, Правобережная улица, 1Б | Северный административный округ | ежедневно, 10:00–22:00 | 55.88 | 37.45 | 5.00 | средние | Средний счёт:400–600 ₽ | 500.00 | NaN | 1 | 148.00 |
name | category | address | district | hours | lat | lng | rating | price | avg_bill | middle_avg_bill | middle_coffee_cup | chain | seats | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
6227 | Хлеб с маслом | булочная | Москва, улица Вавилова, 3 | Южный административный округ | ежедневно, 09:00–22:00 | 55.71 | 37.59 | 4.80 | NaN | NaN | NaN | NaN | 1 | 320.00 |
4621 | ФО Point | ресторан | Москва, улица Сретенка, 1с1 | Центральный административный округ | пн-пт 11:00–23:00; сб,вс 12:00–23:00 | 55.77 | 37.63 | 4.40 | средние | Средний счёт:500–1000 ₽ | 750.00 | NaN | 0 | NaN |
1820 | Ещё одна собачка | кофейня | Москва, улица Дубки, 2 | Северный административный округ | ежедневно, 09:00–21:00 | 55.82 | 37.57 | 4.70 | NaN | Цена чашки капучино:180–250 ₽ | NaN | 215.00 | 0 | NaN |
6831 | Столовая | кафе | Москва, Профсоюзная улица, 83А | Юго-Западный административный округ | ежедневно, 10:00–18:00 | 55.65 | 37.53 | 4.40 | NaN | NaN | NaN | NaN | 0 | 50.00 |
922 | Хей Мам | пиццерия | Москва, улица Коминтерна, 15 | Северо-Восточный административный округ | ежедневно, 10:00–23:00 | 55.87 | 37.69 | 4.60 | NaN | NaN | NaN | NaN | 0 | NaN |
name | category | address | district | hours | lat | lng | rating | price | avg_bill | middle_avg_bill | middle_coffee_cup | chain | seats | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
8401 | Суши Мания | кафе | Москва, Профсоюзная улица, 56 | Юго-Западный административный округ | ежедневно, 09:00–02:00 | 55.67 | 37.55 | 4.40 | NaN | NaN | NaN | NaN | 0 | 86.00 |
8402 | Миславнес | кафе | Москва, Пролетарский проспект, 19, корп. 1 | Южный административный округ | ежедневно, 08:00–22:00 | 55.64 | 37.66 | 4.80 | NaN | NaN | NaN | NaN | 0 | 150.00 |
8403 | Самовар | кафе | Москва, Люблинская улица, 112А, стр. 1 | Юго-Восточный административный округ | ежедневно, круглосуточно | 55.65 | 37.74 | 3.90 | NaN | Средний счёт:от 150 ₽ | 150.00 | NaN | 0 | 150.00 |
8404 | Чайхана Sabr | кафе | Москва, Люблинская улица, 112А, стр. 1 | Юго-Восточный административный округ | ежедневно, круглосуточно | 55.65 | 37.74 | 4.20 | NaN | NaN | NaN | NaN | 1 | 150.00 |
8405 | Kebab Time | кафе | Москва, Россошанский проезд, 6 | Южный административный округ | ежедневно, круглосуточно | 55.60 | 37.60 | 3.90 | NaN | NaN | NaN | NaN | 0 | 12.00 |
data.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 8406 entries, 0 to 8405 Data columns (total 14 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 name 8406 non-null object 1 category 8406 non-null object 2 address 8406 non-null object 3 district 8406 non-null object 4 hours 7870 non-null object 5 lat 8406 non-null float64 6 lng 8406 non-null float64 7 rating 8406 non-null float64 8 price 3315 non-null object 9 avg_bill 3816 non-null object 10 middle_avg_bill 3149 non-null float64 11 middle_coffee_cup 535 non-null float64 12 chain 8406 non-null int64 13 seats 4795 non-null float64 dtypes: float64(6), int64(1), object(7) memory usage: 919.5+ KB
data.duplicated().sum()
0
data.isna().sum()
name 0 category 0 address 0 district 0 hours 536 lat 0 lng 0 rating 0 price 5091 avg_bill 4590 middle_avg_bill 5257 middle_coffee_cup 7871 chain 0 seats 3611 dtype: int64
pd.DataFrame(round(data.isna().mean()*100,)).style.background_gradient('coolwarm')
0 | |
---|---|
name | 0.000000 |
category | 0.000000 |
address | 0.000000 |
district | 0.000000 |
hours | 6.000000 |
lat | 0.000000 |
lng | 0.000000 |
rating | 0.000000 |
price | 61.000000 |
avg_bill | 55.000000 |
middle_avg_bill | 63.000000 |
middle_coffee_cup | 94.000000 |
chain | 0.000000 |
seats | 43.000000 |
Комментарий:
Видим пропуски в столбцах hours
, price
, avg_bill
, middle_avg_bill
, middle_coffee_cup
, seats
Пропусков в данных огромное количество. Просто удалить не получится, будут искажения в исследовании.
Для дальнейшего исследования нам понадабятся столбец middle_avg_bill
. Значения для этого столбца берутся из avg_bill
.
print('Количество заведений:', data.name.nunique())
Количество заведений: 5614
data.category.value_counts()
data.seats.describe()
Комментарий:
В датасете имеется информация о 5614 заведениях питания в Москве. В столбцах name
, category
, address
, district
, hours
, price
и avg_bill
содержится информация о названии, категории, адресе, районе, часах работы, ценовой категории и средней стоимости заказа, представленных в формате объекта. Колонки lat
и lng
предоставляют данные о географических координатах заведения, а middle_avg_bill
и middle_coffee_cup
содержат данные о средней стоимости заказа и чашки капучино соответственно. Столбец chain содержит целочисленные значения 1 и 0, указывающие, относится заведение к сети или нет. Однако, столбец seats содержит данные о количестве посадочных мест в виде чисел с плавающей точкой, что является ошибкой. Кроме того, в некоторых столбцах есть пропущенные значения.
# Данные выглядят корректно, приведем весь текст в нижний регистр.
for col in ['name', 'address', 'avg_bill']:
data[col] = data[col].str.lower()
data.head()
# Воссполним пропуски колонок 'avg_bill', 'price', 'hours'
for column in ['avg_bill', 'price', 'hours']:
data[column] = data[column].fillna('н/д')
Комментарий:
Столбец middle_avg_bill
содержит число с оценкой среднего чека, которое указано только для значений из столбца avg_bill
, начинающихся с подстроки «Средний счёт». Если значения в столбце avg_bill
нет или оно не начинается с подстроки «Средний счёт», то в столбец ничего не войдёт.
Выведем количество непустых значений столбца middle_avg_bill
.
len(data.query('~middle_avg_bill.isna()'))
Найдём количество строк датасета data, для которых значение столбца avg_bill
начинается с подстроки «cредний счёт».
len(data.loc[data['avg_bill'].apply(lambda x: x.find('средний счёт')) != -1])
Комментарий:
Число строк 3149 равно количеству непустых значений в столбце middle_avg_bill
.
Заполним пропуски c помощью модели машинного обучения метод K-ближайших соседей.
imputer = KNNImputer()
middle_avg_bill_2d = data['middle_avg_bill'].values.reshape(-1, 1)
data['middle_avg_bill'] = pd.DataFrame(data=imputer.fit_transform(middle_avg_bill_2d),
columns=['middle_avg_bill'],
index=data.index)
middle_avg_bill_2d = data['middle_coffee_cup'].values.reshape(-1, 1)
data['middle_coffee_cup'] = pd.DataFrame(data=imputer.fit_transform(middle_avg_bill_2d),
columns=['middle_coffee_cup'],
index=data.index)
Комментарий:
Столбец middle_coffee_cup
содержит число с оценкой одной чашки капучино, которое указано только для значений из столбца avg_bill
, начинающихся с подстроки «Цена чашки капучино». Если значения в столбце avg_bill
нет или оно не начинается с подстроки «Цена чашки капучино», то в столбец ничего не войдёт.
Выведем количество непустых значений столбца middle_coffee_cup
.
len(data.query('~middle_coffee_cup.isna()'))
Найдём количество строк датафрейма data, для которых значение столбца avg_bill
начинается с подстроки «Цена чашки капучино».
len(data.loc[data['avg_bill'].apply(lambda x: x.find('цена чашки капучино')) != -1])
Комментарий:
Число строк 535 равно количеству непустых значений в столбце middle_coffee_cup
.
Заполним пропуски в столбце middle_coffee_cup
заглушкой "0".
#data['middle_coffee_cup'] = data['middle_coffee_cup'].fillna(0)
Заменим пропуски в столбце seats заглушкой "-1".
#data['seats'] = data['seats'].fillna(-1)
# Проверим обработку пропусков.
data.isna().sum()
# Посмотрим на дубликаты
data.loc[data.duplicated(subset=['name', 'address'], keep=False)]
Комментарий:
Явным дубликатом выглядит вторая строка с данными ресторана "More Poke" и "кафе" Вторые строки для заведений "Раковарня клешни и хвосты" и "Хлеб да выпечка" содержат разные категории заведений и немного разные координаты. Так как адрес один, будем считать это дубликатами и удалим дублирующие строки.
data.drop_duplicates(subset=['name', 'address'], keep='first', inplace=True)
data.duplicated(subset=['name', 'address']).sum()
Проверим на неявные дубликаты названия категорий заведений и районы.
data['category'].unique()
data['district'].unique()
Неявных дубликатов в столбцах category
и district
нет. Выведем топ-10 самых часто встречающихся названий заведений.
data.groupby('name') \
.agg(count=('name', 'count')) \
.sort_values(by='count', ascending=False) \
.reset_index() \
.head(10) \
.style.background_gradient('coolwarm')
Комментарий:
Очевидно, что большая часть из этих заведний - сетевые. Но встречаются и большое количество заведений с названиями "Кафе", "Ресторан" и "Столовая". Проверим, есть ли среди заведений с такими названиями сетевые.
data.query('(name == "кафе" or name == "ресторан" or name == "столовая") and chain == 1')
Комментарий:
Сетевых нет. Необходимо проверить данные на неявные дубликаты по названиям.
Попробуем поискать неявные дубликаты по первому слову в названии заведения. Для этого создадим столбец с первым словом в названии и поищем дубликаты по этому столбцу, столбцу category
и address
.
data['dup_name'] = data['name'].str.split(' ').str[0]
data.duplicated(subset=['dup_name', 'category', 'address']).sum()
Обнаружено 15 дубликатов. Выведем строки с дубликатами.
data.loc[data.duplicated(subset=['dup_name', 'category', 'address'], keep=False)]
Комментарий:
У этих заведений похожие названия, совпадают категории и адреса. По-видимому, некоторые из них представляют собой разные заведения, находящиеся в одном здании, либо разные залы одного заведения. Часть заведений является дубликатами. Удалим строки, которые по всей видимости являются дубликатами.
data = data.drop(data[(data['name'] == "чебуреки манты") & (data['address'] == "Москва, Правобережная улица, 1Б")].index)
data = data.drop(data[(data['name'] == "чайхана халал") & (data['address'] == "Москва, Смольная улица, 24Г, стр. 6")].index)
data = data.drop(data[(data['name'] == "dragon bubble tea") & (data['address'] == "Москва, Щёлковское шоссе, вл75")].index)
data = data.drop(data[(data['name'] == "баку 24 часа") & (data['address'] == "Москва, Монтажная улица, 9, стр. 1")].index)
data = data.drop(data[(data['name'] == "udcкафе upside down cake") & (data['address'] == "Москва, Кутузовский проспект, 57")].index)
data = data.drop(data[(data['name'] == "vip wok and sushi") & (data['address'] == "Москва, Можайское шоссе, 45Б")].index)
data = data.drop(data[(data['name'] == "от мяса до рыбы") & (data['address'] == "Москва, улица Вавилова, 64/1с1")].index)
data = data.drop(data[(data['name'] == "чайхана дружба") & (data['address'] == "Москва, Большая Очаковская улица, 47А, стр. 1")].index)
data = data.drop(data[(data['name'] == "estetica cafe") & (data['address'] == "Москва, Кировоградская улица, 15")].index)
data = data.drop(data[(data['name'] == "кафе") & (data['address'] == "Москва, Ореховый бульвар, 28")].index)
data.loc[data.duplicated(subset=['dup_name', 'category', 'address'], keep=False)]
Дубликаты обработаны. Удалим столбец dup_name
.
data = data.drop(['dup_name'], axis=1)
# Изменим тип данных на int
#for col in ['middle_avg_bill', 'middle_coffee_cup', 'seats']:
#data[col] = data[col].astype('int')
def make_acronym(phrase):
phrase = phrase.replace('-', ' ').split()
acronym = ""
for word in phrase:
acronym = acronym + word[0].upper()
return acronym
# Добавим акронимы к районам
data['district_short'] = data['district'].apply(make_acronym)
Посмотрим какие категории заведений представлены в данных. Исследуем количество объектов общественного питания по категориям: рестораны, кофейни, пиццерии, бары и так далее. Построим визуализации.
category_name = data.groupby('category')['name'].count().reset_index()
category_name.columns = ['category', 'count']
category_name.style.background_gradient('coolwarm')
#fig = go.Figure(data=[go.Bar(x=category_name['category'], y=category_name['count'])],
# layout=go.Layout(title=go.layout.Title(text="Столбчатая диаграмма категорий заведений"),
# xaxis=go.layout.XAxis(title=go.layout.xaxis.Title(text="Категории")),
# yaxis=go.layout.YAxis(title=go.layout.yaxis.Title(text="Количество заведений")),
# template='plotly_white'))
#fig.show()
fig = px.bar(category_name,
x='category',
y='count',
text='count',
title='Количество объектов общественного питания по видам',
template='plotly_white'
)
fig.update_layout(xaxis_title='Категории заведений',
yaxis_title='Количество заведений',
xaxis={'categoryorder':'total descending'})
fig.show()
Комментарий:
кафе
с наибольшим количеством - 2376 заведений, за ним следуют рестораны
с 2042 заведениями, а наименьшее число заведений у булочных
- всего 256. Однако, кафе и рестораны составляют более чем половину (около 52,6%) от общего числа заведений, в то время как кофейни, бары/пабы и пиццерии представлены чуть менее чем третью частью (33,43%). Оставшиеся заведения, такие как булочные, столовые и быстрое питание занимают 13,97% от общего числа.
Исследуем количество посадочных мест в местах по категориям: рестораны, кофейни, пиццерии, бары и так далее. Построим визуализации.
seats = data.query('seats != -1') \
.groupby('category') \
.agg(seats_median=('seats', 'median')) \
.sort_values(by='seats_median', ascending=False) \
.reset_index()
seats['seats_median'] = seats['seats_median'].astype('int')
# строим столбчатую диаграмму
fig = px.bar(seats.sort_values(by='seats_median', ascending=True), # загружаем данные и заново их сортируем
x='seats_median', # указываем столбец с данными для оси X
y='category', # указываем столбец с данными для оси Y
text='seats_median',
template='plotly_white'# добавляем аргумент, который отобразит текст с информацией
# о количестве объявлений внутри столбца графика
)
# оформляем график
fig.update_layout(title='Количество посадочных мест в объектах общественного питания по категориям',
xaxis_title='Количество посадочных мест',
yaxis_title='Категория объекта общественного питания')
fig.show() # выводим график
Комментарий:
Рестораны предоставляют наибольшее количество мест для посадки, что логично, поскольку такая специализация предполагает отдых и питание гостей. На втором месте - быстрые рестораны, при этом они являются самыми распространенными заведениями. В то же время, кафе и столовые предлагают наименьшее количество мест для посадки.
Подсчитаем число сетевых и несетевых заведений.
chain_category = ['сетевые', 'несетевые']
values = [len(data.query('chain == 1')), len(data.query('chain == 0'))]
fig = go.Figure(data=[go.Pie(labels=chain_category, values=values)])
fig.update_layout(title='Cоотношение сетевых и несетевых заведений общественного питания',
width=800,
height=500,
annotations=[dict(x=1.15,
y=1.05,
text='Категория',
showarrow=False)])
fig.show()
Комментарий:
Судя по диаграмме, можно утверждать, что примерно 62% ресторанов в Москве не принадлежат к какой-либо сети.
# Посмотрим какие категории заведений чаще являются сетевыми
chain_objects = data.groupby(['category', 'chain'])['name'].count().reset_index()
chain_objects.columns = ['object_type', 'chain', 'count']
chain_objects['chain'] = chain_objects['chain'].astype(object)
chain_objects = chain_objects.sort_values(['count', 'chain'])
chain_objects
fig = px.bar(chain_objects,
x='count',
y='object_type',
text='count',
template='plotly_white',
color='chain',
category_orders={"chain": ["сетевой", "несетевой"]}
)
# оформляем график
fig.update_layout(title='Соотношение сетевых заведений',
xaxis_title='Количество заведений',
yaxis_title='Название категорий',
)
fig.show()
Комментарий:
Согласно графику, большинство заведений не относятся к сетевым, за исключением нескольких категорий: для кофеен количество сетевых заведений немного превышает количество независимых (720|693); для пиццерий количество сетевых заведений уже значительно превышает количество независимых (330|303); для булочных разница между сетевыми и независимыми заведениями оказалась самой большой (157|99).
Сгруппируем данные по названиям заведений и найдем топ-15 популярных сетей в Москве.
df_chain = data[data['chain'] == 1]
top_15 = df_chain.groupby('name').agg({'rating' : 'median', 'category' : pd.Series.mode, 'district' : 'count'})
top_15 = top_15.rename(columns={'district':'count'})
top_15 = top_15.sort_values('count', ascending = False).reset_index().head(15)
top_15
fig = px.bar(top_15,
x='count',
y='name',
text='count',
template='plotly_white',
color='name'
)
# оформляем график
fig.update_layout(title='ТОП-15 популярных сетей в Москве',
xaxis_title='Количество заведений',
yaxis_title='Название заведений',
showlegend=False)
fig.show()
Комментарий:
По результатам мы можем заключить, что среди сетевых заведений в Москве первое место занимает Шоколадница, второе и третье места заняли известные пиццерии, а наименьшее количество заведений относится к сети Му-Му.
print('Всего заведений в Топ-15:', top_15['count'].sum())
fig = px.bar(top_15,
x='count',
y='category',
template='plotly_white',
color='category'
)
# оформляем график
fig.update_layout(title='Количество заведений каждой категории по районам',
xaxis_title='Количество заведений',
yaxis_title='Название категорий',
yaxis={'categoryorder':'total ascending'}
)
fig.show()
district_chain = df_chain.groupby(['district', 'category', 'name']).agg({'rating' : 'median', 'address' : 'count'})
district_chain = district_chain.sort_values('rating', ascending = False).reset_index()
district_chain = district_chain.rename(columns={'address':'count'})
district_chain = district_chain[district_chain['name'].isin(top_15['name'])]
district_chain.head()
fig = px.bar(district_chain,
x='count',
y='district',
template='plotly_white',
color='category'
)
# оформляем график
fig.update_layout(title='Количество заведений каждой категории по районам',
xaxis_title='Количество заведений',
yaxis_title='Название района',
yaxis={'categoryorder':'total ascending'}
)
fig.show()
Посмотрим какие административные районы Москвы присутствуют в датасете. Отобразим общее количество заведений и количество заведений каждой категории по районам.
print('Общее количество заведений в датасете:',data['name'].count())
district_chain_df = data.groupby(['district', 'category']).agg({'rating' : 'median', 'name' : 'count'})
district_chain_df = district_chain_df.sort_values('rating', ascending = False).reset_index()
district_chain_df = district_chain_df.rename(columns={'name':'count'})
district_chain_df.head()
fig = px.bar(district_chain_df,
x='count',
y='district',
template='plotly_white',
color='category'
)
# оформляем график
fig.update_layout(title='Количество заведений каждой категории по районам',
xaxis_title='Количество заведений',
yaxis_title='Название района',
yaxis={'categoryorder':'total ascending'}
)
fig.show()
Комментарий:
Мы имеем 9 административных округов. График показывает, что наибольшее количество заведений всех категорий находится в центральном административном округе. В основном, это кафе, кофейни и рестораны, что логично, учитывая, что люди едут в центр города, чтобы отдохнуть, погулять или сделать покупки. Категория кафе имеет неплохое распределение по всем округам. В то же время столовых наименьшее количество в каждом из округов.
Визуализируем распределение средних рейтингов по категориям заведений.
rating_category = data.groupby('category').agg({'rating' : 'mean'}).round(2).sort_values('rating', ascending = False).reset_index()
rating_category
fig = px.bar(rating_category,
x='rating',
y='category',
text='rating',
template='plotly_white',
color='category'
)
# оформляем график
fig.update_layout(title='Распределение средних рейтингов по категориям заведений',
xaxis_title='Рейтинг',
yaxis_title='Название категорий')
fig.update_xaxes(range=[4, 4.5])
fig.show()
Комментарий:
Согласно данным на графике, бары и пабы имеют наивысший рейтинг, тогда как пиццерии, рестораны, кофейни и булочные имеют примерно одинаковый рейтинг. Рестораны быстрого питания имеют наименьший рейтинг. Кроме того, средние значения по всем категориям превышают 4 в нашей выборке.
Построим фоновую картограмму (хороплет) со средним рейтингом заведений каждого района. Границы районов Москвы, которые встречаются в датасете, хранятся в файле admin_level_geomap.geojson
rating_df = data.groupby('district', as_index=False)['rating'].agg('mean').round(2)
rating_df
# читаем файл и сохраняем в переменной
with open('/datasets/admin_level_geomap.geojson', 'r') as f:
geo_json = json.load(f)
# загружаем JSON-файл с границами округов Москвы
state_geo = '/datasets/admin_level_geomap.geojson'
# moscow_lat - широта центра Москвы, moscow_lng - долгота центра Москвы
moscow_lat, moscow_lng = 55.751244, 37.618423
# создаём карту Москвы
m = Map(location=[moscow_lat, moscow_lng], zoom_start=10)
# создаём хороплет с помощью конструктора Choropleth и добавляем его на карту
Choropleth(
geo_data=state_geo,
data=rating_df,
columns=['district', 'rating'],
key_on='feature.name',
#fill_color='YlGn',
#fill_opacity=0.8,
legend_name='Средний рейтинг заведений по районам',
).add_to(m)
# выводим карту
m
На основании предоставленных данных можно сделать вывод о том, что у заведений, расположенных в Центральном административном округе, наивысший рейтинг, составляющий 4.38. В то же время заведения, расположенные в Юго-Восточном административном округе, имеют наименьший рейтинг, который составляет 4.1.
Отобразим все заведения датасета на карте с помощью кластеров средствами библиотеки folium
.
# moscow_lat - широта центра Москвы, moscow_lng - долгота центра Москвы
moscow_lat, moscow_lng = 55.751244, 37.618423
# создаём карту Москвы
m = Map(location=[moscow_lat, moscow_lng], zoom_start=10)
# создаём пустой кластер, добавляем его на карту
marker_cluster = MarkerCluster().add_to(m)
# пишем функцию, которая принимает строку датафрейма,
# создаёт маркер в текущей точке и добавляет его в кластер marker_cluster
def create_clusters(row):
Marker(
[row['lat'], row['lng']],
popup=f"{row['name']} {row['rating']}",
).add_to(marker_cluster)
# применяем функцию create_clusters() к каждой строке датафрейма
data.apply(create_clusters, axis=1)
# выводим карту
m
По карте отчетливо видно, что основная масса заведений сконцентрирована в центре Москвы. Уже меньше на севере и еще меньше на юге.
Найдем топ-15 улиц по количеству заведений. Построим график распределения количества заведений и их категорий по этим улицам.
words = ['проезд','шоссе','улица','переулок','микрорайон','мкад','проспект','пр.',
'площадь','аллея','бульвар','набережная','сквер','тупик','линия','территория',
'квартал','просек','парк','мост']
str_pat = r".*,\s*\b([^,]*?(?:{})\b[^,]*)[,$]+".format("|".join(words))
data['street'] = data['address'].str.extract(str_pat, flags=re.I)
streets_moscow = data[data['street'].notnull()]
top15_streets = streets_moscow['street'].value_counts().reset_index().head(15)
top15_streets.columns = ['street_name', 'count']
top15_streets
#создадим таблицу с названиями улиц и категорий
streets_category = data.groupby(['street', 'category'])['name'].count().reset_index()
streets_category.columns = ['street_name', 'category', 'count']
streets_category.sort_values('count', ascending=False)
#оставим только улицы из топ 15
streets15_category = streets_category[streets_category['street_name'].isin(top15_streets['street_name'])]
streets15_category
fig = px.bar(streets15_category,
x='count',
y='street_name',
template='plotly_white',
color='category'
)
# оформляем график
fig.update_layout(title='Количество заведений каждой категории по районам',
xaxis_title='Количество заведений',
yaxis_title='Название улиц',
yaxis={'categoryorder':'total ascending'}
)
fig.show()
Из графика выше мы видим, что больше всего заведений на проспекте Мира. Преобладают категории кафе и рестораны. Столовых меньше всего. Следом за ним идет Профсоюзная, ситуация с категориями там аналогична. Меньше всего заведений на улица Миклухо-Маклая. Среди популярных категорий также кафе и рестораны.
one_cafe = data['street'].value_counts().reset_index()
one_cafe.columns = ['street_name', 'cafe_count']
one_cafe = one_cafe[one_cafe['cafe_count'] == 1]
one_cafe
#добавим районы
streets1_category = streets_category[streets_category['street_name'].isin(one_cafe['street_name'])]
streets1_category = streets1_category.groupby('category')['street_name'].count()
streets1_category
Из данных выше мы видим, что 425 улиц имеют только одно заведение. Больше всего из них относятся к категории кафе.
Значения средних чеков заведений хранятся в столбце middle_avg_bill
. Эти числа показывают примерную стоимость заказа в рублях, которая чаще всего выражена диапазоном. Посчитаем медиану этого столбца для каждого района. Используем это значение в качестве ценового индикатора района. Построим фоновую картограмму (хороплет) с полученными значениями для каждого района.
median_bill = data.groupby('district')['middle_avg_bill'].median().reset_index()
# создаём карту Москвы
m2 = Map(location=[moscow_lat, moscow_lng], zoom_start=10)
# создаём хороплет с помощью конструктора Choropleth и добавляем его на карту
Choropleth(
geo_data=state_geo,
data=median_bill,
columns=['district', 'middle_avg_bill'],
key_on='feature.name',
legend_name='Средний чек заведений по районам',
).add_to(m2)
# выводим карту
m2
Комментарий:
Из приведенных данных можно сделать вывод, что Центральный и Западный округа имеют наибольший средний чек в сравнении с Юго-Восточным округом, который имеет наименьший средний чек. Округи вблизи Центрального округа имеют в среднем средний чек на 1,5 - 2 раза выше, чем округа вдали от центра.
Анализ показал, что наибольшее количество заведений в Москве - кафе (2378), немного меньше - рестораны (2043), и наименьшее количество - булочные (256).
Кафе и рестораны составляют почти 52,6% от общего числа заведений, в то время как кофейни, бары/пабы и пиццерии составляют 33,43%, а булочные, столовые и заведения быстрого питания - 13,97%. Рестораны предоставляют наибольшее количество посадочных мест, а заведения быстрого питания распространены шире всего. Кофейни занимают первое место по количеству сетевых заведений, за которой следует категория пиццерий.
Самой популярной сетью является Шоколадница. Заведений категории кофе, ресторанов и пиццерий в топ-15 примерно одинаковое количество. Заведений из топ-15 в центральном административном округе больше всего, а в северо-западном - наименьшее количество.
Проспект Мира - улица с наибольшим количеством заведений, преобладают кафе и рестораны, а меньше всего - столовые. Средний чек выше всего в центральном и западном округах, а наименьший - в юго-восточном.
Ответим на следующие вопросы:
Сколько всего кофеен в датасете? В каких районах их больше всего, каковы особенности их расположения?
Есть ли круглосуточные кофейни?
Какие у кофеен рейтинги? Как они распределяются по районам?
На какую стоимость чашки капучино стоит ориентироваться при открытии и почему?
Посчитаем количество и посмотрим на их расположение.
cofe_df = data[data['category'] == 'кофейня']
print('Всего коффен:', cofe_df.shape[0])
# создаём карту Москвы
m3 = Map(location=[moscow_lat, moscow_lng], zoom_start=10)
# создаём пустой кластер, добавляем его на карту
marker_cluster = MarkerCluster().add_to(m3)
# пишем функцию, которая принимает строку датафрейма,
# создаёт маркер в текущей точке и добавляет его в кластер marker_cluster
def create_clusters(row):
Marker(
[row['lat'], row['lng']],
popup=f"{row['name']} {row['rating']}",
).add_to(marker_cluster)
# применяем функцию create_clusters() к каждой строке датафрейма
cofe_df.apply(create_clusters, axis=1)
# выводим карту
m3
У нас имеется 1413 кофейных мест. Большинство из них расположены в Центральном районе. За ними следуют юго-западные и северные части города.
Посмотрим, есть ли круглосуточные кофейни.
coffee = data.query('hours == "ежедневно, круглосуточно" & category == "кофейня"')
coffee_house = data.query('hours == "ежедневно, круглосуточно" & category == "кофейня"') \
.groupby(by=['district_short'], as_index=False) \
.agg(count=('name', 'count'))
print(f'Количество кругосуточных кофеен: {coffee_house["count"].sum()}')
fig = px.bar(
data_frame=coffee_house.sort_values('count',ascending=False),
x='district_short', y='count', color='district_short', text='count',
title='Количество кофеен 24/7 по районам',
labels={'district_short': 'Район', 'count': 'Количество кафе'},
height=450
)
fig.update_layout(
legend_title='Часы работы',
template='plotly_white'
)
fig.show()
# создаём карту Москвы
m4 = Map(location=[moscow_lat, moscow_lng], zoom_start=10)
# создаём пустой кластер, добавляем его на карту
marker_cluster = MarkerCluster().add_to(m4)
# пишем функцию, которая принимает строку датафрейма,
# создаёт маркер в текущей точке и добавляет его в кластер marker_cluster
def create_clusters(row):
Marker(
[row['lat'], row['lng']],
popup=f"{row['name']} {row['rating']}",
).add_to(marker_cluster)
# применяем функцию create_clusters() к каждой строке датафрейма
coffee.apply(create_clusters, axis=1)
# выводим карту
m4
Комментарий:
Из этого можно сделать вывод, что в центре города наиболее распространены круглосуточные кофейни, в то время как в остальных районах это отсутствует или представлено в минимальном количестве.
Посмотрим какие у кофеен рейтинги. Как они распределяются по районам.
rating_cofe = cofe_df.groupby('district', as_index=False)['rating'].agg('mean').round(2).sort_values('rating', ascending=False)
rating_cofe
# создаём карту Москвы
m5 = Map(location=[moscow_lat, moscow_lng], zoom_start=10)
# создаём хороплет с помощью конструктора Choropleth и добавляем его на карту
Choropleth(
geo_data=state_geo,
data=rating_cofe,
columns=['district', 'rating'],
key_on='feature.name',
#fill_color='YlGn',
#fill_opacity=0.8,
legend_name='Средний рейтинг заведений по районам',
).add_to(m5)
# выводим карту
m5
Комментарий:
Центральный административный округ и Северо-Западный административный округ имеют самые высокие показатели рейтинга по сравнению с другими округами, в то время как Западный административный округ имеет наименьший рейтинг.
Посмотрим на какую стоимость чашки капучино стоит ориентироваться при открытии.
middle_cofe = cofe_df.groupby('district', as_index=False)['middle_coffee_cup'].agg('mean').round().sort_values('middle_coffee_cup', ascending=False)
display(middle_cofe)
print('Средняя стоимость чашки кофе в Москве:', middle_cofe['middle_coffee_cup'].mean().round())
# создаём карту Москвы
m6 = Map(location=[moscow_lat, moscow_lng], zoom_start=10)
# создаём хороплет с помощью конструктора Choropleth и добавляем его на карту
Choropleth(
geo_data=state_geo,
data=middle_cofe,
columns=['district', 'middle_coffee_cup'],
key_on='feature.name',
#fill_color='YlGn',
#fill_opacity=0.8,
legend_name='Средний цена чашки кофе по районам',
).add_to(m6)
# выводим карту
m6
Комментарий:
В Центральном, Западном и Юго-Западном округах находится самый дорогой кофе, средняя его стоимость составляет 180, в то время как в среднем по Москве цена за чашку кофе равна 171. Если вы собираетесь открыть новое кафе, рекомендуется определить цену на кофе исходя из района, в котором находится ваше заведение, не превышая средней цены в этом районе.
При запуске новой кофейни рекомендуется выбрать Центральный, Западный или Юго-Западный округа, поскольку здесь средняя стоимость кофе выше, что позволит получить хорошую прибыль. Это также позволит начать дело со сниженной ценой на кофе без больших потерь.
Стоит обдумать возможность круглосуточной работы. Западный и Юго-Западный округа являются дефицитными регионами в плане круглосуточных заведений. В Центральном округе будет лучше воспользоваться форматом 24/7, так как это самый оживленный район с большим количеством пешеходных улиц и оживлен в темное время суток. Кроме того, на западных округах наблюдается низкий рейтинг заведений, что может быть использовано как преимущество при запуске.